package pers.cfeng.server.dataManage.pageCache;

import pers.cfeng.common.AbstractDataCache;
import pers.cfeng.common.DatabaseException;
import pers.cfeng.common.ErrorCode;
import pers.cfeng.server.dataManage.page.Page;
import pers.cfeng.server.dataManage.page.PageImpl;
import pers.cfeng.server.utils.FaultHandler;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author Cfeng
 * pageCache的实现，也就是实现数据库db文件的操作【读取成一个页面】
 * 一页大小8KB,一个db文件就包含从1到n多个数据页
 *
 * 缓存中的内存管理： 也是分为一个个页面
 * 不像redis，这里实现缓存就依靠的是容器Map
 */

public class PageCacheImpl extends AbstractDataCache<Page> implements PageCache {

    private RandomAccessFile raf;
    private FileChannel fileChannel;
    private Lock fileLock; //多线程操作都需要加锁

    //AQS实现页面计数， 主要为了方便新建数据页时正确统计
    private AtomicInteger pageNumbers;

    public PageCacheImpl(int maxResource, RandomAccessFile raf, FileChannel fileChannel) {
        super(maxResource);
        //缓存不够
        if(maxResource < PageCacheConstant.MEM_MIN_LIM)
            FaultHandler.forcedStop(new DatabaseException(ErrorCode.MEM_TOO_SMALL));
        long length = 0;
        try {
            length = raf.length();
        } catch (IOException e) {
            FaultHandler.forcedStop(e);
        }
        this.raf = raf;
        this.fileChannel = fileChannel;
        fileLock = new ReentrantLock();
        this.pageNumbers = new AtomicInteger((int)length/PageCacheConstant.PAGE_SIZE);
    }

    private static long pageOffset(int pageNo) {
        //页号从1开始，所以起始量为(n - 1) * size
        return (pageNo - 1) * PageCacheConstant.PAGE_SIZE;
    }

    //缓存中新建数据页面， 实际上时创建的真实的数据页Page， 之后flush到磁盘文件中
    @Override
    public int newPage(byte[] initData) {
        //利用AQS的原子性自增
        int pageNo = pageNumbers.incrementAndGet();
        Page page = new PageImpl(pageNo, initData, null);
        flush(page);
        return pageNo;
    }


    /**
     * 获取当前页面
     */
    @Override
    public Page getPage(int pageNo) throws Exception {
        return get((long)pageNo);
    }

    /**
     * 关闭文件
     */
    @Override
    public void close() {
        super.close();
        try {
            fileChannel.close();
            raf.close();
        } catch (IOException e) {
            FaultHandler.forcedStop(e);
        }
    }

    /**
     * 引用计数框架下的具体实现，从db中获取数据页Page
     * @param key  代表的时pageNO
     */
    @Override
    protected Page getToCache(long key) throws Exception {
        int pageNo = (int)key;
        long offset = PageCacheImpl.pageOffset(pageNo); //获取指定页的起始位置

        //这里设置缓冲区大小为页面大小，直接读取页
        ByteBuffer buffer = ByteBuffer.allocate(PageCacheConstant.PAGE_SIZE);
        fileLock.lock();
        //文件读写应该加上lock避免并发问题
        try {
            fileChannel.position(offset);
            fileChannel.read(buffer);
        } catch (IOException e) {
            FaultHandler.forcedStop(e);
        }
        fileLock.unlock();
        return new PageImpl(pageNo, buffer.array(), this);
    }

    /**
     * 引用计数框架释放资源
     */
    @Override
    protected void releaseForCache(Page page) {
        if(page.isModified()) {
            //当前页被修改过, 那么需要回写保证一致性
            flush(page);
            page.setModified(false);
        }
    }

    /**
     * 强行释放某一个页面，转换为页号再释放
     */
    @Override
    public void release(Page page) {
        release((long) page.getPageNumber());
    }

    /**
     * 删除pageNO后所有的页面，强行将length设置为pageNO的结尾， 同时pageNumbers设置为pageNo
     */
    @Override
    public void truncateAfterPageNo(int pageNo) {
        long size = pageOffset(pageNo + 1);
        try {
            raf.setLength(size);
        } catch (IOException e) {
            FaultHandler.forcedStop(e);
        }
        //修改AQS量为pageNO， 通过set方法
        pageNumbers.set(pageNo);
    }

    /**
     * 获取当前的页面数量，AQS的intValue即可
     */
    @Override
    public int getPageNo() {
        return pageNumbers.intValue();
    }

    /**
     * 强制刷新某个页面到db中
     * 就是从cache回写到db文件中
     */
    @Override
    public void flushPage(Page page) {
        flush(page);
    }

    public void flush(Page page) {
        int pageNo = page.getPageNumber();
        long offset = pageOffset(pageNo);

        fileLock.lock();
        try {
            ByteBuffer buffer = ByteBuffer.wrap(page.getData());
            fileChannel.position(offset);
            fileChannel.write(buffer);
            fileChannel.force(false);
        } catch (IOException e) {
            FaultHandler.forcedStop(e);
        } finally {
            fileLock.unlock();
        }
    }
}
